package com.tool4j.util.http;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * OkHttp3 工具类
 */
@Slf4j
public class OkHttpUtil {

    private OkHttpClient httpClient;

    public static final String HTTP_TYPE_POST = "post";
    public static final String HTTP_TYPE_PUT = "put";
    public static final String HTTP_TYPE_DELETE = "delete";
    public static final String HTTP_TYPE_GET = "get";

    /**
     * 统一请求地址
     */
    private String host = "";

    private static final MediaType FORM = MediaType.parse("application/x-www-form-urlencoded");

    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");

    private static final MediaType OCTET_STREAM = MediaType.parse("application/octet-stream");

    public static OkHttpUtil build(String host) {
        return build(new Config().setHost(host));
    }

    /**
     * 初始化okHttpClient，并且允许https访问
     *
     * @param config 配置
     * @return
     */
    public static OkHttpUtil build(Config config) {
        if (null == config) {
            config = new Config();
        }
        TrustManager[] trustManagers = buildTrustManagers();
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(config.getConnectTimeout(), TimeUnit.SECONDS)
                .writeTimeout(config.getWriteTimeout(), TimeUnit.SECONDS)
                .readTimeout(config.getReadTimeout(), TimeUnit.SECONDS)
                .retryOnConnectionFailure(config.getRetryOnConnectionFailure());
        /**
         * 忽略证书
         */
        if (config.getIgnoreSSL()) {
            builder.sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0])
                    .hostnameVerifier((hostName, session) -> true);
        }

        builder.addInterceptor(chain -> {
            Request request = chain.request();
            long startTime = System.currentTimeMillis();
            log.info("开始请求: url = {}", request.url());
            Response proceed = chain.proceed(request);
            log.info("请求结束: url = {}, 耗时 = {} ms", request.url(), System.currentTimeMillis() - startTime);
            return proceed;
        });
        return new OkHttpUtil().setHttpClient(builder.build()).setHost(config.getHost());
    }

    /**
     * 设置请求地址
     *
     * @param host
     * @return
     */
    private OkHttpUtil setHost(String host) {
        if (StrUtil.isNotBlank(host)) {
            this.host = host;
        }
        return this;
    }

    /**
     * 设置HttpClient
     *
     * @param httpClient
     * @return
     */
    private OkHttpUtil setHttpClient(OkHttpClient httpClient) {
        this.httpClient = httpClient;
        return this;
    }

    /**
     * GET请求
     *
     * @param url 接口路径
     * @return 接口返回内容
     */
    public ResponseBody get(String url) {
        return get(url, null, null);
    }

    /**
     * GET请求
     *
     * @param url    接口路径
     * @param params 路径携带的参数
     * @return 接口返回内容
     */
    public ResponseBody get(String url, Map<String, Object> params) {
        return get(url, params, null);
    }

    /**
     * GET请求
     *
     * @param url    接口路径
     * @param params 路径携带的参数
     * @param header 请求头
     * @return 接口返回内容
     */
    public ResponseBody get(String url, Map<String, Object> params, Map<String, String> header) {
        if (CollUtil.isNotEmpty(params)) {
            url = buildUrl(url, params);
        }
        Request.Builder requestBuilder = new Request.Builder().url(host + url).get();
        //设置请求头
        setHeader(requestBuilder, header);
        return execute(requestBuilder);
    }

    /**
     * POST请求
     *
     * @param url    接口路径
     * @param params 请求体对象
     * @return 接口返回内容
     */
    public ResponseBody post(String url, Map<String, Object> params) {
        return post(url, params, null);
    }

    /**
     * POST请求
     *
     * @param url    接口路径
     * @param params 请求体对象
     * @param header 请求头
     * @return 接口返回内容
     */
    public ResponseBody post(String url, Map<String, Object> params, Map<String, String> header) {
        Request.Builder requestBodyBuilder = new Request.Builder().url(host + url);
        setHeader(requestBodyBuilder, header);
        if (params != null) {
            RequestBody requestBody = RequestBody.create(JSON, JSONUtil.toJsonStr(params));
            requestBodyBuilder.post(requestBody);
        }
        return execute(requestBodyBuilder);
    }

    /**
     * 指定请求方式
     *
     * @param url      接口路径
     * @param params   请求参数
     * @param httpType 请求类型
     * @param header   请求头
     * @return 接口返回内容
     */
    public ResponseBody request(String url, Object params, String httpType, Map<String, String> header) {
        Request.Builder requestBodyBuilder = new Request.Builder().url(host + url);
        setHeader(requestBodyBuilder, header);
        if (params != null) {
            RequestBody requestBody = RequestBody.create(JSON, JSONUtil.toJsonStr(params));
            switch (httpType) {
                case HTTP_TYPE_PUT:
                    requestBodyBuilder.put(requestBody);
                case HTTP_TYPE_DELETE:
                    requestBodyBuilder.delete(requestBody);
                case HTTP_TYPE_GET:
                    return get(url, BeanUtil.beanToMap(params), header);
                case HTTP_TYPE_POST:
                default:
                    requestBodyBuilder.post(requestBody);
            }
        }
        return execute(requestBodyBuilder);
    }

    /**
     * 文件传输
     *
     * @param url   接口路径
     * @param field 文件名
     * @param file  MultipartFile
     * @return
     */
    public ResponseBody postMultipart(String url, String field, MultipartFile file) throws IOException {
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart(field, file.getName(), RequestBody.create(OCTET_STREAM, file.getBytes()))
                .build();
        Request request = new Request.Builder()
                .url(host + url)
                .post(requestBody)
                .build();
        return execute(request);
    }

    /**
     * 文件传输
     *
     * @param url   接口路径
     * @param field 文件名
     * @param file  文件
     * @return
     */
    public ResponseBody postMultipart(String url, String field, File file) {
        RequestBody requestBody = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart(field, file.getName(), RequestBody.create(OCTET_STREAM, file))
                .build();

        Request request = new Request.Builder()
                .url(host + url)
                .post(requestBody)
                .build();
        return execute(request);
    }

    /**
     * POST请求-表单提交
     *
     * @param url      接口路径
     * @param formBody 表单数据
     * @return 响应内容
     */
    public ResponseBody postForm(String url, Map<String, String> formBody) {
        return postForm(url, formBody, null);
    }

    /**
     * POST请求-表单提交
     *
     * @param url      接口路径
     * @param formBody 表单数据
     * @param header   请求头
     * @return 响应内容
     */
    public ResponseBody postForm(String url, Map<String, String> formBody, Map<String, String> header) {
        Request.Builder requestBodyBuilder = new Request.Builder().url(host + url);
        setHeader(requestBodyBuilder, header);
        if (formBody != null) {
            FormBody.Builder formBodyBuilder = new FormBody.Builder();
            formBody.forEach(formBodyBuilder::add);
            RequestBody requestBody = formBodyBuilder.build();
            requestBodyBuilder.post(requestBody);
        }
        return execute(requestBodyBuilder);
    }

    /**
     * 发送请求
     *
     * @param requestBodyBuilder 请求建造器
     * @return
     */
    private ResponseBody execute(Request.Builder requestBodyBuilder) {
        return execute(requestBodyBuilder.build());
    }

    /**
     * 发送请求
     *
     * @param request 请求体
     * @return ResponseBody 如果响应内容是二进制流，记得关闭！
     */
    private ResponseBody execute(Request request) {
        try {
            Response response = httpClient.newCall(request).execute();
            Assert.isTrue(response.isSuccessful(), "请求出错");
            return response.body();
        } catch (Exception e) {
            log.error("请求出错：", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * get请求地址参数拼接
     *
     * @param url    接口路径
     * @param params 请求参数
     * @return
     */
    private String buildUrl(String url, Map<String, Object> params) {
        StringBuilder urlBuilder = new StringBuilder(url).append("?");
        try {
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                urlBuilder.append(URLEncoder.encode(entry.getKey(), "utf-8")).
                        append("=").
                        append(URLEncoder.encode(entry.getValue().toString(), "utf-8")).
                        append("&");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        urlBuilder.deleteCharAt(urlBuilder.length() - 1);
        return urlBuilder.toString();
    }

    /**
     * 添加请求头
     *
     * @param request 请求建造器
     * @param header  请求头
     */
    private void setHeader(Request.Builder request, Map<String, String> header) {
        if (header != null) {
            try {
                for (Map.Entry<String, String> entry : header.entrySet()) {
                    request.addHeader(entry.getKey(), entry.getValue().toString());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 统一配置
     */
    @Data
    @Accessors(chain = true)
    public static class Config {

        /**
         * 请求地址
         */
        private String host;

        /**
         * 连接超时时间：秒
         */
        private Integer connectTimeout = 15;

        /**
         * 写入超时时间：秒
         */
        private Integer writeTimeout = 20;

        /**
         * 读取超时时间：秒
         */
        private Integer readTimeout = 20;

        /**
         * 忽略SSL证书
         */
        private Boolean ignoreSSL = true;

        /**
         * 将此客户端配置为在遇到连接问题时重试或不重试。默认情况下，该客户端从以下问题中静默恢复:
         * 不可达的IP地址。如果URL的主机有多个IP地址，则无法到达任何单个IP地址不会使整个请求失败。这可以提高多宿主服务的可用性。
         * 陈旧的池连接。ConnectionPool重用套接字以减少请求延迟，但这些连接偶尔会超时。
         * 无法访问代理服务器。ProxySelector可用于依次尝试多个代理服务器，最终退回到直接连接。
         * 将此设置为false以避免重试请求，因为这样做是破坏性的。在这种情况下，调用应用程序应该执行自己的连接故障恢复
         */
        private Boolean retryOnConnectionFailure = false;

    }

    /**
     * 生成安全套接字工厂，用于https请求的证书跳过
     *
     * @return
     */
    public static SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) {
        SSLSocketFactory ssfFactory = null;
        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            ssfFactory = sc.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return ssfFactory;
    }

    public static TrustManager[] buildTrustManagers() {
        return new TrustManager[]{
                new X509TrustManager() {
                    @Override
                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
                    }

                    @Override
                    public X509Certificate[] getAcceptedIssuers() {
                        return new X509Certificate[]{};
                    }
                }
        };
    }

}
